Skip to content

security: [T1-05] fix oracle signature bypass (DGB-SEC-002)#374

Closed
gto90 wants to merge 9 commits intofeature/digidollar-v1from
fix/digidollar-sec-002-oracle-sig-bypass
Closed

security: [T1-05] fix oracle signature bypass (DGB-SEC-002)#374
gto90 wants to merge 9 commits intofeature/digidollar-v1from
fix/digidollar-sec-002-oracle-sig-bypass

Conversation

@gto90
Copy link
Member

@gto90 gto90 commented Feb 13, 2026

Summary

  • DGB-SEC-002: Oracle signature verification in IsValidOracleMessage() Phase One path verified Schnorr signatures against the message's embedded pubkey instead of the chainparams-authorized key, allowing attackers to forge oracle price messages with their own keypair
  • Fix binds the authorized pubkey from chainparams before calling VerifyPhase2() in all 4 verification code paths (P2P gate + 3 defense-in-depth sites)
  • Builds cleanly, zero new warnings

Test plan

  • Verify IsValidOracleMessage() rejects messages signed with unauthorized keys
  • Verify legitimate oracle messages (signed by chainparams keys) still pass validation
  • Run existing oracle unit tests (test_digibyte --run_test=oracle_tests)
  • Verify Phase Two multi-oracle validation still works with bound pubkeys

Bind authorized pubkey from chainparams before Schnorr signature
verification in all code paths. Previously, Phase One validation
called VerifyPhase2() using the message's embedded pubkey, allowing
an attacker to forge oracle price messages with their own keypair.

Fix applied to 4 verification sites:
- IsValidOracleMessage() Phase One path (critical P2P gate)
- IsValidOracleMessage() Phase Two path (already had binding, kept)
- OracleDataValidator::ValidateOracleMessage() (defense-in-depth)
- ValidatePhaseOneBundle() (defense-in-depth)
- ValidatePhaseTwoBundle() (defense-in-depth)

The pattern is: copy the message, overwrite oracle_pubkey with the
chainparams-authorized key, then verify. This ensures signatures
are always checked against trusted keys, never attacker-supplied ones.
Copilot AI review requested due to automatic review settings February 13, 2026 03:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request addresses a critical security vulnerability (DGB-SEC-002) in oracle signature verification. The vulnerability allowed attackers to forge oracle price messages by providing their own keypair, as the signature verification used the message's embedded public key instead of the authorized key from chainparams.

Changes:

  • Fixed 5 oracle signature verification code paths to bind the authorized public key from chainparams before verification
  • Refactored price bounds validation to apply to both Phase One and Phase Two in IsValidOracleMessage()
  • Added detailed security comments explaining the fix at each location

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

OpenSSL 1.1.1w Configure misparses multi-word flags like "-arch arm64"
when passed as positional args — the shell splits them and Configure
treats the second word as a target name, causing "target already
defined" on ARM64 macOS. Pass CFLAGS/CPPFLAGS as VAR=value assignments
per OpenSSL's official INSTALL docs.

libcurl 8.5.0 fopen.c uses fileno/fdopen (POSIX) which are hidden by
the depends system's strict -std=c11. Add -D_GNU_SOURCE for Linux.
Address Copilot review comments on SEC-002 PR:
- Extract duplicated chainparams pubkey binding into a single static
  helper function, reducing 5 inline copies to 1 definition + 5 calls
- Standardize variable naming (remove _p2 suffix)
- Unify comment style across all verification sites
Use $$ deferred evaluation for CFLAGS/CPPFLAGS assignments so
OS-specific flags (cflags_linux, cppflags_linux) appended by funcs.mk
after set_vars are included. Route -fPIC and -D_GNU_SOURCE through
proper cflags/cppflags variables instead of bare config_opts to avoid
OpenSSL's "Mixing make variables" rejection.
The depends system never defines $(package)_arflags, so
ARFLAGS=$($(package)_arflags) in config_env exported an empty
ARFLAGS="" to the environment. When CFLAGS/CPPFLAGS are passed
as VAR=value assignments (not positional flags), OpenSSL 1.1.1w's
Configure sets $anyuseradd=false and falls back to reading env
vars. The empty ARFLAGS overrides the target default "r" from
Configurations/00-base-templates.conf, producing a Makefile with
ARFLAGS= (empty). This causes "ar: two different operation options
specified" on Linux and "ar: illegal option -- /" on macOS during
build_libs, as ar interprets the archive path as flags.

Fix: remove ARFLAGS from config_env entirely, letting Configure
use its target default ARFLAGS="r".

Verified locally:
- Linux x86_64: ar r apps/libapps.a ... (ARFLAGS=r in Makefile)
- macOS ARM64: ar r apps/libapps.a ... (ARFLAGS=r in Makefile)
- Full build_libs completes successfully on darwin64-arm64-cc
Replace BOOST_CHECK with BOOST_CHECK_MESSAGE in 5 mint validation
tests that fail on CI but pass locally. Diagnostic output captures
the exact reject reason, script sizes, collateral amounts, and
script type identification to help debug the CI-specific failure.
Replace ClearFreeze() with ClearHistory() in the validation test
fixture. ClearFreeze() only clears freeze flags, but UpdateState()
(called during ValidateDigiDollarTransaction) recalculates volatility
from stale price history left by earlier test suites (e.g.,
digidollar_health_tests) and re-sets the freeze. This caused all
"valid mint" tests to fail with "minting-frozen-volatility" on CI
where the test suite ordering (linker-dependent) places health_tests
before validation_tests.

ClearHistory() resets price history, volatility state, and all freeze
flags, ensuring each test starts from a clean state.
Keep diagnostic BOOST_CHECK_MESSAGE over base branch's
BOOST_TEST_MESSAGE for better CI failure reporting.
The merge with feature/digidollar-v1 silently dropped critical code:

1. DD amount double-counting prevention in ValidateMintTransaction()
   The OP_RETURN and DD token output both encode ddAmount. Without the
   consistency check, totalDD was counted twice (e.g. 10000 + 10000 =
   20000), requiring 2x the collateral and failing all valid mint tests.

2. Missing #include <digidollar/health.h>

3. Improved log format for insufficient-collateral diagnostic

Also adopts the base branch's test file which includes:
- DD OP_RETURN outputs required for T1-04b NUMS verification
- Proper NUMS key usage via GetCollateralNUMSKey()
- ClearHistory() fix for cross-suite volatility state pollution
@DigiSwarm
Copy link

Closing as duplicate — already fixed on feature/digidollar-v1.

Why this is superseded:

This PR creates a VerifyOracleMessageWithChainparamsKey() helper and applies it to 5 verification sites in bundle_manager.cpp. The core fix (verifying oracle signatures against chainparams pubkeys instead of the attacker-supplied embedded pubkey) was already applied in our overnight red team session as commit f995f92ad6 (T1-05a) and the broader oracle security fixes.

The refactoring into a single helper function is a nice cleanup, but the security fix itself is already in place at all verification sites. The approach differs slightly (we fixed each site individually vs a centralized helper), but the security outcome is identical.

Thanks @gto90 for the thorough analysis of the oracle signature bypass — this was one of the more critical findings.

@DigiSwarm DigiSwarm closed this Feb 13, 2026
@gto90 gto90 deleted the fix/digidollar-sec-002-oracle-sig-bypass branch February 14, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants